import ChessBoard from './ChessBoard';
import {Chess, ChessHistory, ForbidRules, BanPosition, BanPositionMap, PositionScore} from './Constants';

/**
 * 机器人对象，用户对局的对手，游戏规则的辅助控制
 */
class Robot {
    /** 棋盘对象 */
    chessBoard : ChessBoard;
    /** 机器人所执棋子颜色 */
    chessType = Chess.WHITE;
    /** 机器人是否是先手 */
    robotFirst = false;

    /** 禁手规则，暂时不考虑放入构造函数，为的是后面要加入重开时可以更改规则的功能 */
    forbidRules : ForbidRules = { doubleFour:false, doubleThree:false, moreThanFive:false};

    constructor(chessBoard : ChessBoard){
        this.chessBoard = chessBoard;
    }

    changeConfig(chessType:Chess, robotFirst:boolean, forbidRules : ForbidRules){
        this.chessType = chessType;
        this.robotFirst = robotFirst;
        this.forbidRules = forbidRules;
    }

    /**
     * 机器人落子功能，获取机器人落子的坐标
     * @param grid 棋盘状态对象
     * @param totalStep 当前总步数
     */
    getChessPosition(grid:Chess[][], totalStep:number) : ChessHistory{
        let res;
        if(totalStep<3){
            res = this.getCenterPosition(grid);
        } else {
            this.banPositionMap = {};
            this.scoreObj = {};
            res = this.getLinePosition(grid);
        }
        return res;
    }

    /** 在找不到合适的落子的位置的时候，优先选一个靠近期盼中心的位置来落子 */
    private getCenterPosition(grid:Chess[][]) : ChessHistory {
        let res = {x:-1, y:-1, type:this.chessType};

        // 这个方法一定能找到可以落子的位置，所以不用担心返回的 res 对象的 x y 值是 -1，从而导致数组越界报错
        this.centerToBorderSearch(grid, (arr)=>{
            res.x = arr[0];
            res.y = arr[1];
            return false;
        });

        return res;
    }

    /** 找一个可以形成一条直线的位置 */
    private getLinePosition(grid:Chess[][])  : ChessHistory {
        // 因为随着局势的变化，有些禁手后面很可能就不是禁手了，所以每次都要重置禁手状态，并重新判断
        for (let x = 0; x < grid.length; x++) {
           for (let y = 0; y < grid[x].length; y++) {
                if(grid[x][y].startsWith(Chess.EMPTY)){
                    grid[x][y] = Chess.EMPTY;
                }
           }
        }
        return this.getPositonFromLine5(grid, 1, false) || this.getPositonFromLine5(grid, 2, false) || 
                    this.getPositonFromLine5(grid, 3, true) || this.getCenterPosition(grid);
    }

    /**
     * 找到一个在落子后，可以形成有效的 5 子一线的位置
     * @param grid 棋盘棋子位置对象
     * @param chessType 棋子类型
     * @param avliadCount 有效的落子数量（表示还差avliadCount个棋子就可以形成5子了）
     * @param searchMore 是否需要多检测一次有效的落子数量加一的情况
     * @returns 空白位置
     */
    private getPositonFromLine5(grid:Chess[][], avliadCount:number, searchMore:boolean) : ChessHistory | undefined{
        const opponentChess = this.chessType===Chess.BLACK?Chess.WHITE:Chess.BLACK;
        // 每次都要遍历所有的空白位置，然后打分，选择得分最高的地方落子
        for (let x = 0; x < grid.length; x++) {
            for (let y = 0; y < grid[x].length; y++) {
                this.findEmptyPosition(x, y, grid, avliadCount, this.chessType, 0);
                this.findEmptyPosition(x, y, grid, avliadCount, opponentChess, -1);
                if(searchMore){
                    // 这里为了提高准确度，也会多遍历一次有效落子数量多一子的情况
                    this.findEmptyPosition(x, y, grid, avliadCount + 1, this.chessType, 0);
                    this.findEmptyPosition(x, y, grid, avliadCount + 1, opponentChess, -1);
                }
                // TODO 还差检测死三的情况（遇到死三需要把分数减一）,这里说的死三是指三子一侧已经有对手堵住了，只能在另外一侧落2子才能形成 5 子
                // TODO 还差禁止形成的 4-3 情况（又称混合禁手），这种情况还没有进行过充分测试
            }
        }
        let resPosition : ChessHistory = {x:-1, y:-1, type:this.chessType};
        let maxCore = 4 - avliadCount;
        let scoreItem;
        for(let key in this.scoreObj){ // 打分的位置都是空白位置，所以下面不需要再重复判断是否是空白位置了
            scoreItem = this.scoreObj[key];
            if(scoreItem.score>=maxCore){ 
                maxCore = scoreItem.score;
                resPosition.x = scoreItem.x;
                resPosition.y = scoreItem.y;
            }
        }
        if(resPosition.x===-1){
            return undefined;
        }
        return resPosition;
    }

    private banPositionMap : BanPositionMap = {}; // 线路统计，用于判断是否是禁手
    private scoreObj : PositionScore = {};  // 打分对象

    /**
     * 从某个起始点开始，向下，向右，向左下，向右下 四个方向寻找可以连成5子的空白位置，
     * 由于是从圆点（0，0）递归搜索，所以不需要向上、向左、向右上、向左上这四个方向重复搜索
     * @param x x 坐标
     * @param y y 坐标
     * @param grid 棋盘数据
     * @param avliadCount 空白位置数量
     * @param chessType 棋子类型
     * @param scoreOffset 分数修正量
     * @returns 打分对象
     */
    private findEmptyPosition(x: number, y: number, grid: Chess[][], avliadCount: number, chessType: Chess, scoreOffset:number) {
        const lineArray = this.getLineArray(x, y);
        let resPosition : {x:number,y:number}[];
        for (let index = 0; index < lineArray.length; index++) {
            if(lineArray[index][0].x < 0 || lineArray[index][0].x >= grid.length
                || lineArray[index][0].y < 0 || lineArray[index][0].y >= grid[x].length
                || lineArray[index][4].x < 0 || lineArray[index][4].x >= grid.length
                || lineArray[index][4].y < 0 || lineArray[index][4].y >= grid[x].length){
                continue;
            }
            let direct = index;//Math.floor(index/5);// 同一个方向有5条线路
            resPosition = this.findPosition(grid, lineArray[index], avliadCount, chessType);
            resPosition.forEach((item)=>{
                let key = item.x + "-" + item.y;
                // 只有活二、活三才需要判断禁手（活二添一子会形成活三，活三添一子会形成活四，只有这两种情况才会形成双三双四）
                // 只判断先手禁手
                if(   ( (this.forbidRules.doubleFour && avliadCount===2)  || 
                        (this.forbidRules.doubleThree && avliadCount===3) || 
                        (this.forbidRules.moreThanFive && avliadCount===1) )
                    && ((this.robotFirst && chessType===this.chessType) || (!this.robotFirst && chessType!==this.chessType)) ){
                    if(!this.banPositionMap[key]){
                        this.banPositionMap[key] = { x:item.x, y:item.y, count:1, lineDirect:[direct], type : avliadCount };
                    }
                    if(!this.banPositionMap[key].lineDirect.includes(direct)){ // 避免同一条线重复计算
                        this.banPositionMap[key].count++;
                        this.banPositionMap[key].lineDirect.push(direct);
                    }
                }
                // 打分统计
                if(!this.scoreObj[key]) 
                    this.scoreObj[key] = { score:0, x:item.x, y:item.y};
                this.scoreObj[key].score = this.scoreObj[key].score + 5 - avliadCount + scoreOffset;
            });
        }
        // 禁手判断  只判断先手禁手
        if(  ( (this.forbidRules.doubleFour && avliadCount===2)  || 
                (this.forbidRules.doubleThree && avliadCount===3) || 
                (this.forbidRules.moreThanFive && avliadCount===1) )
            && ((this.robotFirst && chessType===this.chessType) || (!this.robotFirst && chessType!==this.chessType))
        ) {
            this.banPostionToGrid(chessType, grid);
        }
    }

    /**
     * 获取包含某一个坐标的所有能连成一条线的所有坐标的集合
     */
    private getLineArray(x: number, y: number) : {x:number,y:number}[][] {
        return [
            [{ x, y }, { x: x + 1, y }, { x: x + 2, y }, { x: x + 3, y }, { x: x + 4, y }],  // 横向
            //[{ x: x - 1, y }, { x, y }, { x: x + 1, y }, { x: x + 2, y }, { x: x + 3, y }],
            //[{ x: x - 2, y }, { x: x - 1, y }, { x, y }, { x: x + 1, y }, { x: x + 2, y }],
            //[{ x: x - 3, y }, { x: x - 2, y }, { x: x - 1, y }, { x, y }, { x: x + 1, y }],
            //[{ x: x - 4, y }, { x: x - 3, y }, { x: x - 2, y }, { x: x - 1, y }, { x, y }],

            [{ x, y }, { x, y: y + 1 }, { x, y: y + 2 }, { x, y: y + 3 }, { x, y: y + 4 }],  // 竖向
            //[{ x, y: y - 1 }, { x, y }, { x, y: y + 1 }, { x, y: y + 2 }, { x, y: y + 3 }],
            //[{ x, y: y - 2 }, { x, y: y - 1 }, { x, y }, { x, y: y + 1 }, { x, y: y + 2 }],
            //[{ x, y: y - 3 }, { x, y: y - 2 }, { x, y: y - 1 }, { x, y }, { x, y: y + 1 }],
            //[{ x, y: y - 4 }, { x, y: y - 3 }, { x, y: y - 2 }, { x, y: y - 1 }, { x, y }],

            [{ x, y }, { x: x + 1, y: y + 1 }, { x: x + 2, y: y + 2 }, { x: x + 3, y: y + 3 }, { x: x + 4, y: y + 4 }], // 反斜线 “\”，左上朝右下方向
            //[{ x: x - 1, y: y - 1 }, { x, y }, { x: x + 1, y: y + 1 }, { x: x + 2, y: y + 2 }, { x: x + 3, y: y + 3 }],
            //[{ x: x - 2, y: y - 2 }, { x: x - 1, y: y - 1 }, { x, y }, { x: x + 1, y: y + 1 }, { x: x + 2, y: y + 2 }],
            //[{ x: x - 3, y: y - 3 }, { x: x - 2, y: y - 2 }, { x: x - 1, y: y - 1 }, { x, y }, { x: x + 1, y: y + 1 }],
            //[{ x: x - 4, y: y - 4 }, { x: x - 3, y: y - 3 }, { x: x - 2, y: y - 2 }, { x: x - 1, y: y - 1 }, { x, y }],

            [{ x, y }, { x: x - 1, y: y + 1 }, { x: x - 2, y: y + 2 }, { x: x - 3, y: y + 3 }, { x: x - 4, y: y + 4 }], // 正斜线 “/”，右上朝左下方向
            //[{ x: x + 1, y: y - 1 }, { x, y }, { x: x - 1, y: y + 1 }, { x: x - 2, y: y + 2 }, { x: x - 3, y: y + 3 }],
            //[{ x: x + 2, y: y - 2 }, { x: x + 1, y: y - 1 }, { x, y }, { x: x - 1, y: y + 1 }, { x: x - 2, y: y + 2 }],
            //[{ x: x + 3, y: y - 3 }, { x: x + 2, y: y - 2 }, { x: x + 1, y: y - 1 }, { x, y }, { x: x - 1, y: y + 1 }],
            //[{ x: x + 4, y: y - 4 }, { x: x + 3, y: y - 3 }, { x: x + 2, y: y - 2 }, { x: x + 1, y: y - 1 }, { x, y }]
        ];
    }

    /**
     * 将 banPosition 统计的禁手状态赋值给棋盘对象 grid 对应的坐标
     * @param chessType 棋子类型
     * @param grid 棋盘对象
     * @param avliadCount 现在找的有效棋子个数（表示凑齐这些个数的棋子就可以形成5子一线的局势并获胜）
     */
    private banPostionToGrid(chessType: Chess, grid: Chess[][]) {
        const banType = ("empty-ban-" + chessType) === Chess.BAN_BLACK ? Chess.BAN_BLACK : Chess.BAN_WHITE;
        let p : BanPosition;
        for (let key in this.banPositionMap) {
            p = this.banPositionMap[key];
            // 不使用banPosition[key].count===2来判断，是因为极端情况下，可能存在添一子后，形成不止两路可以连成 5 子的情况出现，所以使用：p.count > 1
            if ((this.forbidRules.moreThanFive  && this.chessCountGreaterFive(grid, p, chessType)) || p.count > 1) {
                grid[p.x][p.y] = banType;
                if(banType.indexOf(this.chessType)>-1){
                    delete this.scoreObj[key];  // 删除禁手的打分，不予考虑在禁手位置落子,避免做不必要的遍历
                }
            } 
        }
    }

    /**
     * 判断包含 BanPosition 坐标在内的连成的一条线的棋子数量是否超过了5
     * @param grid 棋盘对象
     * @param p this.banPosition 对象统计的某一个坐标点
     * @param chessType 棋子类型
     * @returns 
     */
    private chessCountGreaterFive(grid: Chess[][], p : BanPosition, chessType: Chess):boolean{
        for (let index = 0; index < p.lineDirect.length; index++) {
            let chessCount = 0, startX = p.x, startY = p.y;
            const directAction = (searchDirect:number)=>{
                if(p.lineDirect[index]===0){ // 横向  <5
                    startX = startX + searchDirect;
                } else if(p.lineDirect[index]===1){ // 竖向 <10
                    startY = startY + searchDirect;
                } else if(p.lineDirect[index]===2){ // 反斜线 “\”  <15
                    startX = startX + searchDirect;
                    startY = startY + searchDirect;
                } else if(p.lineDirect[index]===3){ // 正斜线 “/”  <20
                    startX = startX - searchDirect;
                    startY = startY + searchDirect;
                }
                if(startX<0 || startX>=grid.length || startY<0 || startY>=grid[0].length){
                    return false;
                }
                return grid[startX][startY]===chessType;
            };
            while(directAction(1)){
                chessCount++;
            }
            startX = p.x;
            startY = p.y;
            while(directAction(-1)){
                chessCount++;
            } 
            if(chessCount>4){
                return true;
            }
        }
        return false;
    }

    /**
     * 从中心向边缘遍历棋盘二维数组，找到一个可以落子的空白位置，然后调用 action 函数
     * @param grid 棋盘棋子位置对象
     * @param action 函数的参数是一个两个元素的数组，第一个元素是 x 坐标，第二个元素是 y 坐标，函数返回 true 表示继续寻找下一个可以落子的空白位置，返回 false 表示终止寻找
     */
    private centerToBorderSearch(grid:Chess[][], action=(arr:number[])=>{return false;}) {
        let nextX = Math.floor(grid.length/2);
        let nextY = Math.floor(grid[0].length/2);
        let direct = [  // 搜索路径只有四种情况，然后不断循环；
            [-1, 0], // 表示 x 坐标依次递减，y坐标表示不变；
            [0, -1], // 表示 x 坐标不变，y 坐标依次递减
            [1, 0],  // 表示 x 坐标依次递增，y坐标表示不变；
            [0, 1]   // 表示 x 坐标不变，y 坐标依次递增
        ];
        let len = 0;
        let directNow, resArr;
        for (let i = 0; ; i++) {
            directNow = direct[i%direct.length];
            len = Math.floor(i/2) + 1;
            resArr = searchPosition(grid, nextX, nextY, directNow, len);
            if(resArr && !action(resArr)){
                break;
            }
            nextX = nextX + len * directNow[0];
            nextY = nextY + len * directNow[1];
            if (nextX < 0 || nextX >= grid.length || nextY < 0 || nextY >= grid[nextX].length) {
                break;
            }
        }
    }

    /**
     * 从数组中 arr 找出可以落子的空白位置：必须有指定数量（emptyCount）的空子，剩下的位置的棋子必须是指定的类型：chessType
     * @param grid 棋盘棋子位置对象
     * @param arr 查找的位置
     * @param emptyCount 空白未落子的位置的总数
     * @param chessType 棋子类型
     * @returns 带上机器人棋子类型的空白位置坐标
     */
    private findPosition(grid:Chess[][], arr:{x:number,y:number}[], emptyCount:number, chessType : Chess) : {x:number,y:number}[] {
        let emptyArr = [];
        const anotherType = chessType===Chess.BLACK?Chess.WHITE:Chess.BLACK;
        let chessTemp ;
        for (let index = 0; index < arr.length && emptyArr.length<=emptyCount; index++) {
            chessTemp = grid[arr[index].x][arr[index].y];
            if((chessTemp.startsWith(Chess.EMPTY) && chessTemp.indexOf(chessType)>-1 )
                || chessTemp===anotherType || chessTemp===Chess.BAN_ALL){
                return [];
            } else if(chessTemp.startsWith(Chess.EMPTY) && chessTemp.indexOf(chessType)<0){
                emptyArr.push(arr[index]);
            }
        }
        if(emptyArr.length===emptyCount){
            return emptyArr;
        }
        return [];
    }
}

/**
 * 从给定的起始坐标开始，根据方向 direct 到指定距离 len ，找一个靠近中心点且未落子的位置
 * @param grid 棋子数据
 * @param x 起始点 x 坐标
 * @param y 七十点 y 坐标
 * @param direct 查找方向，第一个元素是 x 轴方向，第二个元素是 y 轴方向，0表示不变，1表示向前移动(递增)，-1表示向后移动(递减)
 * @param len 查找长度
 * @returns 数组坐标，第一个元素是 x 轴，第二个元素是 y 轴
 */
function searchPosition(grid:Chess[][], x:number, y:number, direct:number[], len:number) : number[] | undefined {
    let tmp;
    if (direct[0] != 0) {
        for (let i = 0; i < len; i++) {
            tmp = x + i * direct[0];
            if (tmp < 0 || tmp >= grid.length)
                break;
            if(grid[tmp][y].startsWith(Chess.EMPTY) && grid[tmp][y]!==Chess.BAN_ALL){
                return [tmp, y];
            }
        }
    } else {
        for (let i = 0; i < len; i++) {
            tmp = y + i * direct[1];
            if (tmp < 0 || tmp >= grid[x].length)
                break;
            if(grid[x][tmp].startsWith(Chess.EMPTY) && grid[tmp][y]!==Chess.BAN_ALL){
                return [x, tmp];
            }
        }
    }
}

export default Robot;